/************** O T T R   C O P Y R I G H T   N O T I C E **************
 **                                                                   **
 ** © 2013 OTTR, Inc.  All rights reserved.                           **
 **                                                                   ** 
 *************** O T T R   C O P Y R I G H T   N O T I C E *************/

package com.jonathan.javatar;

import java.nio.charset.StandardCharsets;

/**
 *
 * @author Jonathan Paz <jonathan.paz@ottr.com>
 */
class GNUTarHeader extends TarHeader {
    public static final int NAME_OFFSET = 0;
    public static final int NAME_LENGTH = 100;
    public static final int MODE_OFFSET = 100;
    public static final int MODE_LENGTH = 8;
    public static final int MODE_MAXVAL = 07777; // maximum allowable given definition, not theoretical maximum
    public static final int UID_OFFSET = 108;
    public static final int UID_LENGTH = 8;
    public static final int UID_MAXVAL = 07777777;
    public static final int GID_OFFSET = 116;
    public static final int GID_LENGTH = 8;
    public static final int GID_MAXVAL = 07777777;
    public static final int SIZE_OFFSET = 124;
    public static final int SIZE_LENGTH = 12;
    public static final long SIZE_MAXVAL = 077777777777L;
    public static final int MTIME_OFFSET = 136;
    public static final int MTIME_LENGTH = 12;
    public static final long MTIME_MAXVAL =  4_297_967_295L; //037_777_777_777L;
    public static final long MTIME_MINVAL = -4_294_967_296L; //040_000_000_000L;
    public static final int CHKSUM_OFFSET = 148;
    public static final int CHKSUM_LENGTH = 8;
    public static final int CHKSUM_MAXVAL = 07777777;
    public static final String CHKSUM_BLANK = "        ";
    public static final int TYPEFLAG_OFFSET = 156;
    public static final int TYPEFLAG_LENGTH = 1;
    public static final int LINKNAME_OFFSET = 157;
    public static final int LINKNAME_LENGTH = 100;
    public static final int MAGIC_OFFSET = 257;
    public static final int MAGIC_LENGTH = 6;
    public static final int UNAME_OFFSET = 265;
    public static final int UNAME_LENGTH = 32;
    public static final int GNAME_OFFSET = 297;
    public static final int GNAME_LENGTH = 32;
    public static final int DEVMAJOR_OFFSET = 329;
    public static final int DEVMAJOR_LENGTH = 8;
    public static final int DEVMAJOR_MAXVAL = 07777777;
    public static final int DEVMINOR_OFFSET = 337;
    public static final int DEVMINOR_LENGTH = 8;
    public static final int DEVMINOR_MAXVAL = 07777777;
    public static final int ATIME_OFFSET = 345;
    public static final int ATIME_LENGTH = 12;
    public static final long ATIME_MAXVAL =  4_297_967_295L; //037_777_777_777L;
    public static final long ATIME_MINVAL = -4_294_967_296L; //040_000_000_000L;
    public static final int CTIME_OFFSET = 357;
    public static final int CTIME_LENGTH = 12;
    public static final long CTIME_MAXVAL =  4_297_967_295L; //037_777_777_777L;
    public static final long CTIME_MINVAL = -4_294_967_296L; //040_000_000_000L;

    private String name;
    private int mode;
    private int uid;
    private int gid;
    private long size;
    private long mtime;
    private int chksum;
    private char typeflag;
    private String linkname;
    private String magic;
    private String uname;
    private String gname;
    private int devmajor;
    private int devminor;
    private long atime;
    private long ctime;
    transient private byte[] headerBlock;

    transient volatile boolean dirtyHeader;

    public GNUTarHeader(byte[] block) {
        super(block);
        int nextHeaderOffset = -1;
        magic = new String(block, MAGIC_OFFSET, Math.min(firstNullAfter(block,MAGIC_OFFSET,MAGIC_LENGTH),MAGIC_LENGTH), StandardCharsets.US_ASCII);
        uname = new String(block, UNAME_OFFSET, Math.min(firstNullAfter(block,UNAME_OFFSET,UNAME_LENGTH),UNAME_LENGTH), StandardCharsets.US_ASCII);
        gname = new String(block, GNAME_OFFSET, Math.min(firstNullAfter(block,GNAME_OFFSET,GNAME_LENGTH),GNAME_LENGTH), StandardCharsets.US_ASCII);
        devmajor = processIntField(block, DEVMAJOR_OFFSET, DEVMAJOR_LENGTH);
        devminor = processIntField(block, DEVMINOR_OFFSET, DEVMINOR_LENGTH);
        switch (typeflag) {
            case 'K':
                nextHeaderOffset = ((int)getSize() + 511) / 512;
                break;
            case 'L':
                nextHeaderOffset = ((int)getSize() + 511) / 512;
                break;
        }
    }

    static int processIntField(byte[] block, int offset, int length) {
        if (block[offset] > 0b10000000) {
            int val = 0;
            for (int i = 1; i < length; i++) {
                int right = 0x000000ff & (int)block[offset+i];
                if ((~(val^right)&(val^(val+right))) >= 0) {
                    val += right;
                } else {
                    val = Integer.MAX_VALUE;
                }
            }
            if (block[offset] == 0b11111111) {
                if (val == Integer.MAX_VALUE) {
                    val = Integer.MIN_VALUE;
                } else {
                    val *= -1;
                }
            }
            return val;
        }
        return Integer.parseInt(new String(block, offset, Math.min(firstNullOrSpaceAfter(block,offset),length), StandardCharsets.US_ASCII), 8);
    }

    static long processLongField(byte[] block, int offset, int length) {
        if (block[offset] > 0b10000000) {
            long val = 0;
            for (int i = 1; i < length; i++) {
                long right = 0x000000ff & (int)block[offset+i];
                if ((~(val^right)&(val^(val+right))) >= 0) {
                    val += right;
                } else {
                    val = Long.MAX_VALUE;
                }
            }
            if (block[offset] == 0b11111111) {
                if (val == Long.MAX_VALUE) {
                    val = Long.MIN_VALUE;
                } else {
                    val *= -1;
                }
            }
            return val;
        }
        return Integer.parseInt(new String(block, offset, Math.min(firstNullOrSpaceAfter(block,offset),length), StandardCharsets.US_ASCII), 8);
    }

    /**
     * The name of the file this record represents.
     * @return the file name
     */
    public String getName() {
        return name;
    }

    /**
     * Sets the name of the file this record represents. This method assumes that
     * the name separator is the '/' character.
     * @param name the file name
     * @throws IllegalArgumentException if the name is longer than {@code NAME_LENGTH}
     */
    public void setName(String name) throws IllegalArgumentException {
        if (name != null && name.length() > NAME_LENGTH) {
            throw new IllegalArgumentException("Name is too long");
        }
        this.name = name;
        dirtyHeader = true;
    }

    /**
     * The mode of the file this record represents.
     * @return mode of the file
     */
    public int getMode() {
        return mode;
    }

    /**
     * Sets the mode of the file this record represents.
     * @param mode the mode of the file
     * @throws IllegalArgumentException if the mode is greater than {@code MODE_MAXVAL} or if it is less than zero
     */
    public void setMode(int mode) throws IllegalArgumentException {
        if (mode > MODE_MAXVAL || mode < 0) {
            throw new IllegalArgumentException("Invalid mode value");
        }
        this.mode = mode;
        dirtyHeader = true;
    }

    /**
     * The ID of the owner of the file
     * @return uid of the file
     */
    public int getUid() {
        return uid;
    }

    /**
     * Sets the ID of the owner of the file
     * @param uid uid of the file
     * @throws IllegalArgumentException if uid is greater than {@code UID_MAXVAL} or if it is less than zero
     */
    public void setUid(int uid) throws IllegalArgumentException {
        if (uid > UID_MAXVAL || uid < 0) {
            throw new IllegalArgumentException("Invalid uid value");
        }
        this.uid = uid;
        dirtyHeader = true;
    }

    /**
     * The ID of the group owner of the file
     * @return gid of the file
     */
    public int getGid() {
        return gid;
    }

    /**
     * Sets the ID of the group owner of the file
     * @param gid gid of the file
     * @throws IllegalArgumentException if gid is greater than {@code GID_MAXVAL} or if it is less than zero
     */
    public void setGid(int gid) throws IllegalArgumentException {
        if (gid > GID_MAXVAL || gid < 0) {
            throw new IllegalArgumentException("Invalid gid value");
        }
        this.gid = gid;
        dirtyHeader = true;
    }

    /**
     * The size of the file this record represents
     * @return file size
     */
    public long getSize() {
        return size;
    }

    /**
     * Sets the size of the file this record represents
     * @param size file size
     * @throws IllegalArgumentException if size is greater than {@code SIZE_MAXVAL} or if it less than zero
     */
    public void setSize(long size) throws IllegalArgumentException {
        if (size > SIZE_MAXVAL || size < 0) {
            throw new IllegalArgumentException("Invalid size value");
        }
        this.size = size;
        dirtyHeader = true;
    }

    /**
     * The last modified time of the file
     * @return file mtime
     */
    public long getMtime() {
        return mtime;
    }

    /**
     * Sets the last modified time of the file
     * @param mtime file mtime
     * @throws IllegalArgumentException if mtime is greater than {@code MTIME_MAXVAL} or if it is less than {@code MTIME_MINVAL}
     */
    public void setMtime(long mtime) throws IllegalArgumentException {
        if (mtime > MTIME_MAXVAL || mtime < MTIME_MINVAL) {
            throw new IllegalArgumentException("Invalid mtime value");
        }
        this.mtime = mtime;
        dirtyHeader = true;
    }

    /**
     * The checksum of this record
     * @return the checksum of this record
     */
    public int getChksum() {
        return chksum;
    }

    /**
     * The type of the file represented by this record
     * @return the file type
     */
    public char getTypeflag() {
        return typeflag;
    }

    /**
     * Sets the type of the file represented by this record
     * @param typeflag the file type
     */
    public void setTypeflag(char typeflag) {
        this.typeflag = typeflag;
        dirtyHeader = true;
    }

    /**
     * The name of the file to which this file links
     * @return the target of this link
     */
    public String getLinkname() {
        return linkname;
    }

    /**
     * Sets the name of the file to which this file links
     * @param linkname the target of this link
     * @throws IllegalArgumentException if the name is longer than {@code LINKNAME_LENGTH}
     */
    public void setLinkname(String linkname) throws IllegalArgumentException {
        if (linkname != null && linkname.length() > LINKNAME_LENGTH) {
            throw new IllegalArgumentException("Linkname is too long");
        }
        this.linkname = linkname;
        dirtyHeader = true;
    }

    /**
     * The magic string for this record. By definition, it is "ustar  ".
     * @return the magic string
     */
    public String getMagic() {
        return magic;
    }

    /**
     * The name of the owner of the file this record represents
     * @return the user name
     */
    public String getUname() {
        return uname;
    }

    public void setUname(String uname) throws IllegalArgumentException {
        if (uname.length() > UNAME_LENGTH) {
            throw new IllegalArgumentException("uanme is too long");
        }
        this.uname = uname;
    }

    public String getGname() {
        return gname;
    }

    public void setGname(String gname) throws IllegalArgumentException {
        if (uname.length() > UNAME_LENGTH) {
            throw new IllegalArgumentException("uanme is too long");
        }
        this.gname = gname;
    }

    public int getDevmajor() {
        return devmajor;
    }

    public void setDevmajor(int devmajor) throws IllegalArgumentException {
        if (devmajor > DEVMAJOR_MAXVAL || devmajor < 0) {
            throw new IllegalArgumentException("Invalid mode value");
        }
        this.devmajor = devmajor;
    }

    public int getDevminor() {
        return devminor;
    }

    public void setDevminor(int devminor) throws IllegalArgumentException {
        if (devminor > DEVMINOR_MAXVAL || devminor < 0) {
            throw new IllegalArgumentException("Invalid mode value");
        }
        this.devminor = devminor;
    }

    public long getAtime() {
        return atime;
    }

    public void setAtime(long atime) {
        if (atime > ATIME_MAXVAL || atime < ATIME_MINVAL) {
            throw new IllegalArgumentException("Invalid mode value");
        }
        this.atime = atime;
    }

    public long getCtime() {
        return ctime;
    }

    public void setCtime(long ctime) {
        if (ctime > CTIME_MAXVAL || ctime < CTIME_MINVAL) {
            throw new IllegalArgumentException("Invalid mode value");
        }
        this.ctime = ctime;
    }

    @Override
    public byte[] getHeaderBlock() {
        if (dirtyHeader) {
            calculateChksum();
        }
        return headerBlock;
    }

    byte[] prepopulateHeader() {
        byte[] header = new byte[512];
        byte[] namebytes = getName().getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(namebytes, 0, header, NAME_OFFSET, Math.min(namebytes.length,NAME_LENGTH));
        byte[] modebytes = String.format("%07o ",getMode()).getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(modebytes, 0, header, MODE_OFFSET, modebytes.length);
        byte[] uidbytes = String.format("%07o ",getUid()).getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(uidbytes, 0, header, UID_OFFSET, uidbytes.length);
        byte[] gidbytes = String.format("%07o ",getGid()).getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(gidbytes, 0, header, GID_OFFSET, gidbytes.length);
        byte[] sizebytes = String.format("%011o ",getSize()).getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(sizebytes, 0, header, SIZE_OFFSET, sizebytes.length);
        byte[] mtimebytes = String.format("%011o ",getMtime()).getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(mtimebytes, 0, header, MTIME_OFFSET, mtimebytes.length);
        byte[] blankchksumbytes = CHKSUM_BLANK.getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(blankchksumbytes, 0, header, CHKSUM_OFFSET, CHKSUM_LENGTH);
        header[TYPEFLAG_OFFSET] = (byte)getTypeflag();
        byte[] linknamebytes = getLinkname().getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(linknamebytes, 0, header, LINKNAME_OFFSET, Math.min(linknamebytes.length, LINKNAME_LENGTH));
        byte[] magicbytes = magic.getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(magicbytes, 0, header, MAGIC_OFFSET, Math.min(magicbytes.length, MAGIC_LENGTH));
        byte[] unamebytes = uname.getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(unamebytes, 0, header, UNAME_OFFSET, Math.min(unamebytes.length, UNAME_LENGTH));
        byte[] gnamebytes = gname.getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(gnamebytes, 0, header, GNAME_OFFSET, Math.min(gnamebytes.length, GNAME_LENGTH));
        byte[] devmajorbytes = String.format("%011o ",devmajor).getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(devmajorbytes, 0, header, DEVMAJOR_OFFSET, devmajorbytes.length);
        byte[] devminorbytes = String.format("%011o ",devminor).getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(devminorbytes, 0, header, DEVMINOR_OFFSET, devminorbytes.length);
        byte[] atimebytes = String.format("%011o ",atime).getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(atimebytes, 0, header, ATIME_OFFSET, atimebytes.length);
        byte[] ctimebytes = String.format("%011o ",ctime).getBytes(StandardCharsets.US_ASCII);
        System.arraycopy(ctimebytes, 0, header, CTIME_OFFSET, ctimebytes.length);
        return header;
    }
    
    @Override
    void calculateChksum() {
        if (dirtyHeader) {
            synchronized(this) {
                if (dirtyHeader) {
                    int checksum = 0;
                    byte[] header = prepopulateHeader();
                    for (byte b : header) {
                        checksum += (0x000000ff & b);
                    }
                    byte[] chksumbytes = String.format("%07o",checksum).getBytes(StandardCharsets.US_ASCII);
                    System.arraycopy(chksumbytes, 0, header, CHKSUM_OFFSET, chksumbytes.length);
                    headerBlock = header;
                    dirtyHeader = false;
                }
            }
        }
    }

    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e); // should not happen anyways.
        }
    }
    
}
